package com.dapeng.cleardatabackend.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dapeng.cleardatabackend.annotation.MultiTransactional;
import com.dapeng.cleardatabackend.common.ErrorCode;
import com.dapeng.cleardatabackend.common.LoginUserHolder;
import com.dapeng.cleardatabackend.exception.BusinessException;
import com.dapeng.cleardatabackend.exception.ThrowUtils;
import com.dapeng.cleardatabackend.mapper.GraphMapper;
import com.dapeng.cleardatabackend.model.dto.graph.GraphAddRequest;
import com.dapeng.cleardatabackend.model.dto.graph.GraphGetRequest;
import com.dapeng.cleardatabackend.model.dto.graph.GraphSearchRequest;
import com.dapeng.cleardatabackend.model.dto.graph.GraphUpdateRequest;
import com.dapeng.cleardatabackend.model.entity.Graph;
import com.dapeng.cleardatabackend.model.entity.Node;
import com.dapeng.cleardatabackend.model.entity.Relation;
import com.dapeng.cleardatabackend.model.entity.User;
import com.dapeng.cleardatabackend.model.enums.GraphStatusEnum;
import com.dapeng.cleardatabackend.model.enums.IsEnum;
import com.dapeng.cleardatabackend.model.enums.MatchModeEnum;
import com.dapeng.cleardatabackend.model.enums.SearchModeEnum;
import com.dapeng.cleardatabackend.model.vo.GraphVO;
import com.dapeng.cleardatabackend.model.vo.graphdetail.Category;
import com.dapeng.cleardatabackend.model.vo.graphdetail.GraphDetailVO;
import com.dapeng.cleardatabackend.model.vo.graphdetail.Link;
import com.dapeng.cleardatabackend.service.GraphService;
import com.dapeng.cleardatabackend.service.NodeService;
import com.dapeng.cleardatabackend.service.RelationService;
import com.dapeng.cleardatabackend.utils.CsvUtils;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.neo4j.driver.Value;
import org.springframework.beans.BeanUtils;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static com.dapeng.cleardatabackend.contant.GraphConstant.MYSQL_BATCH_SIZE;
import static com.dapeng.cleardatabackend.contant.GraphConstant.TEXT_MAX_LENGTH;
import static com.dapeng.cleardatabackend.contant.UserConstant.USER_LOGIN_STATE;

/**
 * @author dapeng
 * @description 针对表【graph(图谱表)】的数据库操作Service实现
 * @createDate 2024-12-22 13:29:10
 */
@Service
public class GraphServiceImpl extends ServiceImpl<GraphMapper, Graph>
        implements GraphService {

    @Resource
    private Neo4jClient neo4jClient;

    @Resource
    private RelationService relationService;

    @Resource
    private NodeService nodeService;

    @Override
    public Long createGraph(GraphAddRequest graphAddRequest) {
        // 1.尝试创建图谱，获取图谱id
        User loginUser = LoginUserHolder.getLoginUser();
        Graph graph = new Graph();
        BeanUtils.copyProperties(graphAddRequest, graph);
        graph.setUserId(loginUser.getId());
        graph.setStatus(GraphStatusEnum.CREATING);
        boolean b = save(graph);
        ThrowUtils.throwIf(!b, ErrorCode.PARAMS_ERROR, "创建图谱失败");
        // 2.获取关系文件和节点文件列表
        MultipartFile nodeFile = graphAddRequest.getNodeFile();
        MultipartFile relationFile = graphAddRequest.getRelationFile();
        Integer relationColumnIndex = graphAddRequest.getRelationColumnIndex();
        Integer startIdColumnIndex = graphAddRequest.getStartIdColumnIndex();
        Integer endIdColumnIndex = graphAddRequest.getEndIdColumnIndex();
        String relationFileSplitSymbol = graphAddRequest.getRelationFileDelimiter();
        Long id = graph.getId();
        // 3.异步获取关系和节点列表
        CompletableFuture<List<Relation>> relationListFuture = CompletableFuture.supplyAsync(() -> getRelationList(relationFile, relationFileSplitSymbol, startIdColumnIndex, endIdColumnIndex, relationColumnIndex, id));
        Integer nodeIdColumnIndex = graphAddRequest.getNodeIdColumnIndex();
        Integer nodeLabelColumnIndex = graphAddRequest.getNodeLabelColumnIndex();
        Integer nodeTextColumnIndex = graphAddRequest.getNodeTextColumnIndex();
        String nodeFileSplitSymbol = graphAddRequest.getNodeFileDelimiter();
        CompletableFuture<List<Node>> nodeListFuture = CompletableFuture.supplyAsync(() -> getNodeList(nodeFile, nodeFileSplitSymbol, nodeIdColumnIndex, nodeLabelColumnIndex, nodeTextColumnIndex, id));
        List<Relation> relationList;
        List<Node> nodeList;
        try {
            relationList = relationListFuture.join();
            nodeList = nodeListFuture.join();
        } catch (Exception e) {
            graph.setStatus(GraphStatusEnum.FAILED);
            baseMapper.updateById(graph);
            throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getCause().getMessage());
        }
        List<Relation> finalRelationList = relationList;
        List<Node> finalNodeList = nodeList;
        // 4.创建索引，加快关系匹配速度
        createIndex(id);
        // 5.异步创建关系和节点
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            boolean saveRelation = relationService.saveBatch(finalRelationList, MYSQL_BATCH_SIZE);
            boolean saveNode = nodeService.saveBatch(finalNodeList, MYSQL_BATCH_SIZE);
            ThrowUtils.throwIf(!saveRelation || !saveNode, ErrorCode.OPERATION_ERROR);
        });
        CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
            nodeService.saveNodeToNeo4j(finalNodeList, id);
            relationService.saveRelationToNeo4j(finalRelationList, id);
        });
        CompletableFuture<Void> future = CompletableFuture.allOf(future1, future2);
        future.whenComplete((v, t) -> {
            if (t == null) {
                graph.setStatus(GraphStatusEnum.SUCCESS);
                baseMapper.updateById(graph);
            } else {
                graph.setStatus(GraphStatusEnum.FAILED);
                baseMapper.updateById(graph);
                relationService.hardRemoveByGraphId(id);
                nodeService.hardRemoveByGraphId(id);
                String query = String.format("MATCH (a:`%s`)-[r]->(b:`%s`) DELETE r", id, id);
                String queryDeleteNodes = String.format("MATCH (n:`%s`) DELETE n", id);
                neo4jClient.query(query).run();
                neo4jClient.query(queryDeleteNodes).run();
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "图谱创建失败");
            }
        }).join();
        return id;
    }


    /**
     * 创建索引，必须创建，会很慢
     *
     * @param graphId 图id
     */
    public void createIndex(Long graphId) {
        String indexQuery = String.format(
                "CREATE INDEX `%s` FOR (n:`%s`) ON (n.nodeId);",
                graphId, graphId
        );
        neo4jClient.query(indexQuery).run();
    }

    /**
     * 获取关系列表
     *
     * @param file                文件
     * @param delimiter           拆分符号
     * @param startIdColumnIndex  前键列索引
     * @param endIdColumnIndex    结束键列索引
     * @param relationColumnIndex 关系列索引
     * @param graphId             图谱id
     * @return {@link List }<{@link Relation }>
     */
    public List<Relation> getRelationList(MultipartFile file, String delimiter, Integer startIdColumnIndex, Integer endIdColumnIndex, Integer relationColumnIndex, Long graphId) {
        Set<Relation> relationSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
            CSVFormat csvFormat = CsvUtils.getCsvFormat(delimiter);
            Iterable<CSVRecord> records = csvFormat.parse(reader);
            Long rowNum = 0L;
            for (CSVRecord record : records) {
                rowNum++;
                ThrowUtils.throwIf(record.size() != 3, ErrorCode.PARAMS_ERROR, String.format("关系文件第%d条记录解析错误，请检查文件！", rowNum));
                String startId = record.get(startIdColumnIndex);
                ThrowUtils.throwIf(startId != null && startId.length() > TEXT_MAX_LENGTH, ErrorCode.PARAMS_ERROR,
                        String.format("前键id长度不能超过%d， 请检查关系文件第%d条记录！", TEXT_MAX_LENGTH, rowNum));
                String relationText = record.get(relationColumnIndex);
                ThrowUtils.throwIf(relationText != null && relationText.length() > TEXT_MAX_LENGTH, ErrorCode.PARAMS_ERROR,
                        String.format("关系文本长度不能超过%d， 请检查关系文件第%d条记录！", TEXT_MAX_LENGTH, rowNum));
                String endId = record.get(endIdColumnIndex);
                ThrowUtils.throwIf(endId != null && endId.length() > TEXT_MAX_LENGTH, ErrorCode.PARAMS_ERROR,
                        String.format("后键id长度不能超过%d， 请检查关系文件第%d条记录！", TEXT_MAX_LENGTH, rowNum));
                Relation relation = new Relation();
                relation.setStartNodeId(startId);
                relation.setRelationText(relationText);
                relation.setEndNodeId(record.get(endIdColumnIndex));
                relation.setGraphId(graphId);
                relationSet.add(relation);
            }
        } catch (IOException e) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR);
        }
        return new ArrayList<>(relationSet);
    }

    /**
     * 获取节点列表
     *
     * @param file             文件
     * @param delimiter        节点文件分割符号
     * @param labelColumnIndex 标签列索引
     * @param textColumnIndex  文本列索引
     * @param graphId          图谱id
     * @return {@link List }<{@link Node }>
     */
    public List<Node> getNodeList(MultipartFile file, String delimiter, Integer nodeIdColumnIndex, Integer labelColumnIndex, Integer textColumnIndex, Long graphId) {
        Set<Node> nodeSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
            CSVFormat csvFormat = CsvUtils.getCsvFormat(delimiter);
            Iterable<CSVRecord> records = csvFormat.parse(reader);
            Long rowNum = 0L;
            for (CSVRecord record : records) {
                rowNum++;
                ThrowUtils.throwIf(record.size() != 3, ErrorCode.PARAMS_ERROR, String.format("节点文件第%d条记录解析错误，请检查文件！", rowNum));
                String nodeId = record.get(nodeIdColumnIndex);
                ThrowUtils.throwIf(nodeId != null && nodeId.length() > TEXT_MAX_LENGTH,
                        ErrorCode.PARAMS_ERROR, String.format("节点id长度不能大于%s，请检查节点文件第%d条记录！", TEXT_MAX_LENGTH, rowNum));
                String label = record.get(labelColumnIndex);
                ThrowUtils.throwIf(label != null && label.length() > TEXT_MAX_LENGTH,
                        ErrorCode.PARAMS_ERROR, String.format("标签长度不能大于%s，请检查节点文件第%d条记录！", TEXT_MAX_LENGTH, rowNum));
                String text = record.get(textColumnIndex);
                ThrowUtils.throwIf(text != null && text.length() > TEXT_MAX_LENGTH,
                        ErrorCode.PARAMS_ERROR, String.format("文本长度不能大于%s，请检查节点文件第%d条记录！", TEXT_MAX_LENGTH, rowNum));
                Node node = new Node();
                node.setNodeId(nodeId);
                node.setLabel(label);
                node.setText(text);
                node.setGraphId(graphId);
                nodeSet.add(node);
            }
        } catch (Exception e) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
        }
        return new ArrayList<>(nodeSet);
    }

    @Override
    @MultiTransactional
    public boolean deleteGraph(Long id) {
        Graph graph = getById(id);
        User loginUser = LoginUserHolder.getLoginUser();
        ThrowUtils.throwIf(graph == null || graph.getStatus() != GraphStatusEnum.SUCCESS || !loginUser.getId().equals(graph.getUserId()), ErrorCode.OPERATION_ERROR, "图谱不存在");
        relationService.deleteRelationByGraphId(id);
        nodeService.deleteNodeByGraphId(id);
        boolean delete = removeById(id);
        ThrowUtils.throwIf(!delete, ErrorCode.OPERATION_ERROR, "删除图谱失败");
        return true;
    }

    @Override
    public void updateGraph(GraphUpdateRequest graphUpdateRequest) {
        User loginUser = LoginUserHolder.getLoginUser();
        Graph graph = getById(graphUpdateRequest.getId());
        ThrowUtils.throwIf(graph == null || graph.getStatus() != GraphStatusEnum.SUCCESS || !loginUser.getId().equals(graph.getUserId()), ErrorCode.OPERATION_ERROR, "图谱不存在");
        Graph newGraph = new Graph();
        BeanUtils.copyProperties(graphUpdateRequest, newGraph);
        boolean update = this.updateById(newGraph);
        ThrowUtils.throwIf(!update, ErrorCode.OPERATION_ERROR, "更新图谱失败");
    }

    @Override
    public Page<GraphVO> getMyGraphPage(GraphGetRequest graphGetRequest) {
        String name = graphGetRequest.getName();
        IsEnum isPublic = graphGetRequest.getIsPublic();
        IsEnum isPublish = graphGetRequest.getIsPublish();
        int current = graphGetRequest.getCurrent();
        int pageSize = graphGetRequest.getPageSize();
        User loginUser = LoginUserHolder.getLoginUser();
        LambdaQueryWrapper<Graph> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(isPublic != null, Graph::getIsPublic, isPublic)
                .eq(isPublic != null, Graph::getIsPublish, isPublish)
                .eq(Graph::getUserId, loginUser.getId())
                .eq(Graph::getStatus, GraphStatusEnum.SUCCESS)
                .like(!StrUtil.isBlankIfStr(name), Graph::getName, name);
        Page<Graph> page = this.page(new Page<>(current, pageSize), queryWrapper);
        Page<GraphVO> pageVO = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        pageVO.setRecords(page.getRecords().stream().map(GraphVO::beanToVo).collect(Collectors.toList()));
        return pageVO;
    }

    @Override
    public Page<GraphVO> getPublicGraphPage(GraphGetRequest graphGetRequest) {
        String name = graphGetRequest.getName();
        IsEnum isPublish = graphGetRequest.getIsPublish();
        int current = graphGetRequest.getCurrent();
        int pageSize = graphGetRequest.getPageSize();
        return baseMapper.selectPublicGraphPage(new Page<>(current, pageSize), name, isPublish);
    }

    @Override
    public GraphDetailVO searchGraph(GraphSearchRequest graphSearchRequest) {
        Long id = graphSearchRequest.getId();
        SearchModeEnum searchMode = graphSearchRequest.getSearchMode();
        String keyword = graphSearchRequest.getKeyword();
        String startText = graphSearchRequest.getStartText();
        String relationText = graphSearchRequest.getRelationText();
        String endText = graphSearchRequest.getEndText();
        MatchModeEnum matchMode = graphSearchRequest.getMatchMode();
        Integer pathLength = graphSearchRequest.getPathLength();
        Integer pathCount = graphSearchRequest.getPathCount();
        String matchStr = matchMode == MatchModeEnum.ACCURATE ? "=" : "CONTAINS";

        Graph graph = getById(id);
        ThrowUtils.throwIf(graph == null || graph.getStatus() != GraphStatusEnum.SUCCESS, ErrorCode.PARAMS_ERROR, "图谱不存在");

        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        User loginUser = (User) request.getSession().getAttribute(USER_LOGIN_STATE);
        if (loginUser == null) {
            ThrowUtils.throwIf(graph.getIsPublish() == IsEnum.NO, ErrorCode.PARAMS_ERROR, "图谱不存在");
        } else {
            ThrowUtils.throwIf((!loginUser.getId().equals(graph.getUserId()) && graph.getIsPublic() == IsEnum.NO), ErrorCode.PARAMS_ERROR, "图谱不存在");
        }

        StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append(String.format("MATCH path=(:`%s`)-[r:`%s`*..%d]->(:`%s`)", id, id, pathLength, id));

        boolean hasCondition = false;
        switch (searchMode) {
            case NODE:
                if (!StrUtil.isBlankIfStr(keyword)) {
                    queryBuilder.append(" WHERE ANY(n IN nodes(path) WHERE n.text ")
                            .append(matchStr).append(" $keyword)");
                }
                break;
            case RELATION:
                if (!StrUtil.isBlankIfStr(keyword)) {
                    queryBuilder.append(" WHERE ANY(rel IN relationships(path) WHERE rel.relation ")
                            .append(matchStr).append(" $keyword)");
                }
                break;
            case LABEL:
                if (!StrUtil.isBlankIfStr(keyword)) {
                    queryBuilder.append(" WHERE ANY(n IN nodes(path) WHERE n.label ")
                            .append(matchStr).append(" $keyword)");
                }
                break;
            case NODE_AND_RELATION:
                if (!StrUtil.isBlankIfStr(startText)) {
                    queryBuilder.append(" WHERE ANY(n IN nodes(path) WHERE n.text ")
                            .append(matchStr).append(" $startText)");
                    hasCondition = true;
                }
                if (!StrUtil.isBlankIfStr(relationText)) {
                    queryBuilder.append(hasCondition ? " AND " : " WHERE ")
                            .append("ANY(rel IN relationships(path) WHERE rel.relation ")
                            .append(matchStr).append(" $relationText)");
                    hasCondition = true;
                }
                if (!StrUtil.isBlankIfStr(endText)) {
                    queryBuilder.append(hasCondition ? " AND " : " WHERE ")
                            .append("ANY(n IN nodes(path) WHERE n.text ")
                            .append(matchStr).append(" $endText)");
                }
                break;
            default:
                break;
        }

        queryBuilder.append(" WITH path ")
                .append(" LIMIT $pathCount ")
                .append(" WITH ")
                .append("   [n IN nodes(path) | { id: ID(n), label: n.label, text: n.text }] AS nodes, ")
                .append("   [rel IN relationships(path) | { source: ID(startNode(rel)), target: ID(endNode(rel)), relation: rel.relation }] AS relations ")
                .append(" UNWIND nodes AS n ")
                .append(" WITH collect(DISTINCT n) AS nodes, relations ")
                .append(" UNWIND relations AS rel ")
                .append(" WITH nodes, collect(DISTINCT rel) AS relations ")
                .append(" RETURN nodes, relations");

        String query = queryBuilder.toString();
        HashMap<String, Object> params = new HashMap<>();
        params.put("keyword", keyword);
        params.put("startText", startText);
        params.put("endText", endText);
        params.put("relationText", relationText);
        params.put("pathCount", pathCount);

        Set<com.dapeng.cleardatabackend.model.vo.graphdetail.Node> nodes = new HashSet<>();
        Set<Link> links = new HashSet<>();
        Set<Category> categories = new HashSet<>();

        neo4jClient.query(query)
                .bindAll(params)
                .fetchAs(Object.class)
                .mappedBy((typeSystem, record) -> {
                    Value nodesValue = record.get("nodes");
                    Value relationsValue = record.get("relations");

                    // 处理 nodes
                    if (nodesValue != null && !nodesValue.isNull()) {
                        List<Object> nodeList = nodesValue.asList();
                        for (Object obj : nodeList) {
                            if (obj instanceof Map) {
                                Map<String, Object> nodeMap = (Map<String, Object>) obj;
                                Long nodeId = (Long) nodeMap.get("id");
                                String label = (String) nodeMap.get("label");
                                String text = (String) nodeMap.get("text");

                                // 分类
                                Category category = new Category();
                                category.setName(label);
                                categories.add(category);

                                // 节点
                                com.dapeng.cleardatabackend.model.vo.graphdetail.Node node = new com.dapeng.cleardatabackend.model.vo.graphdetail.Node();
                                node.setId(nodeId);
                                node.setName(text);
                                node.setCategoryText(label);
                                nodes.add(node);
                            }
                        }
                    }

                    // 处理 relations
                    if (relationsValue != null && !relationsValue.isNull()) {
                        List<Object> relList = relationsValue.asList();
                        for (Object obj : relList) {
                            if (obj instanceof Map) {
                                Map<String, Object> relMap = (Map<String, Object>) obj;
                                Long sourceId = (Long) relMap.get("source");
                                Long targetId = (Long) relMap.get("target");
                                String relation = (String) relMap.get("relation");

                                if (sourceId == null || targetId == null || relation == null) {
                                    continue;
                                }

                                Link link = new Link();
                                link.setSource(sourceId);
                                link.setTarget(targetId);
                                link.setValue(relation);
                                links.add(link);
                            }
                        }
                    }

                    return new Object();
                })
                .all();


        // Category → 下标映射
        AtomicInteger index = new AtomicInteger(0);
        Map<String, Integer> categoryMap = categories.stream().collect(Collectors.toMap(
                Category::getName,
                c -> index.getAndIncrement(),
                (oldValue, newValue) -> oldValue
        ));

        nodes.forEach(node -> node.setCategory(categoryMap.get(node.getCategoryText())));

        GraphDetailVO graphDetailVO = new GraphDetailVO();
        graphDetailVO.setId(id);
        graphDetailVO.setName(graph.getName());
        graphDetailVO.setNodes(new ArrayList<>(nodes));
        graphDetailVO.setLinks(new ArrayList<>(links));
        graphDetailVO.setCategories(categories);
        return graphDetailVO;
    }


    /**
     * 删除索引
     *
     * @param graphId 图id
     */
    public void deleteIndex(Long graphId) {
        String query = String.format("CALL db.indexes() YIELD name WHERE name = '%s' RETURN count(*) > 0 AS exists", graphId);
        Optional<Map<String, Object>> optional = neo4jClient.query(query).fetch().first();
        if (!optional.isPresent()) {
            return;
        }
        if ((boolean) optional.get().get("exists")) {
            String dropIndexQuery = String.format("DROP INDEX `%s`", graphId);
            neo4jClient.query(dropIndexQuery).run();
        }
    }

    @Override
    @MultiTransactional
    public void clearFailCreate() {
        List<Graph> graphs = baseMapper.selectList(new LambdaQueryWrapper<Graph>().eq(Graph::getStatus, GraphStatusEnum.FAILED));
        if (CollectionUtil.isNotEmpty(graphs)) {
            graphs.forEach(graph -> deleteGraph(graph.getId()));
        }
    }

    /**
     * 删除索引
     */
    @Override
    @Transactional(transactionManager = "neo4jTransactionManager")
    public void deleteUnUsedIndex() {
        List<Graph> graphs = baseMapper.selectList(new LambdaQueryWrapper<Graph>().eq(Graph::getStatus, GraphStatusEnum.FAILED));
        if (CollectionUtil.isNotEmpty(graphs)) {
            graphs.forEach(graph -> deleteIndex(graph.getId()));
        }
    }
}